/*
 * Author: Rolando Fentanes
 * Class: Rules
 * The rules class controls how the game is played. It decides how the board
 * can be set up, how pieces can move, what happens after a move,
 * etc. The rules are followed by the game, pieces, and players.
 */

import java.util.ArrayList;

class Rules {
    // decreasing counter that tells the number of pieces available
    // to be placed by type and player
    ArrayList<Counter> piecesAvailable;
    // contains a reference to the board list
    ArrayList<Board> boards;
    // current player
    Player currentPlayer;
    // true if warp came before jump
    Boolean warpFirstMove;

    class Counter {
        String name;
        String player;
        int n;

        Counter(String name, String player, int n) {
            this.name = name;
            this.player = player;
            this.n = n;
        }
    }

    Rules(ArrayList<Board> boardlist) {
        this.boards = boardlist;
    }

    // sets current player
    public void setCurrentPlayer(Player newPlayer) {
        this.currentPlayer = newPlayer;
    }

    // creates a list of valid moves a certain piece can make
    ArrayList<Move> getAvailableMoves(Piece piece, int move) {
        ArrayList<Move> moves = new ArrayList();
        boolean canMoveTo, jump, jumpsIncluded = false, canOnlyWarp = false, jumpsAndWarpsOnly = false, warp;

        // first check if this piece can move at all
        if(piece.moveRange == 0)
            return moves; // returns an empty list
        
        // if this is the second move, then the previous move had better be a jump
        if(move == 2) {
            if(!piece.hasJumped) {
                // unless it can warp and move in the same turn
                if(!piece.canWarpAndMove)
                    return moves;
                // figuring out if the first move was a warp or normal move
                else if(piece.hasWarped)
                    warpFirstMove = true;
                else {
                    warpFirstMove = false;
                    // this far, a piece has not jumped yet, it can warp and jump in the same turn,
                    // and it's the second turn, thus it can only warp
                    canOnlyWarp = true;
                }
            }
            else {
                warpFirstMove = false;
                if(!piece.canWarpAndMove)
                    // assume jumps are included so that only jumps are added to moves list
                    jumpsIncluded = true;
                else
                    // only jumps or warps, if there are not jumps, will be included
                    jumpsAndWarpsOnly = true;
            }
        }
        // if the current move is the third or more,
        // then a chain has to happen for the piece to keep moving
        else if(move > 2) {
            if(!piece.hasJumped)
                // but if it has not jumped after 2 moves, it can't do more
                return moves;
            else if(warpFirstMove)
                // if the piece warped in the first move, then from now on it can only jump
                jumpsIncluded = true;
            // otherwise it means it jumped in the first move
            else if(piece.hasWarped)
                // and if it warped in the second move, it cant move anymore
                return moves;
            else if(piece.canWarpAndMove)
                // else it means it must have jumped in the first and second move
                jumpsAndWarpsOnly = true;
            else
                jumpsIncluded = true;
        }
        
        // goes through each direction a piece can travel to
        for(Location dir: piece.movePositions) {
            int range = piece.moveRange;
            jump = false;
            search:
            for(int i = 1; i != range+1; i++) {
                try {
                    canMoveTo = true; // assume piece can move here and then search otherwise
                    int x = piece.location.x+i*dir.x;
                    int y = piece.location.y+i*dir.y;
                    int z = piece.location.board+i*dir.board;

                    if(z == piece.location.board) {
                        if(canOnlyWarp)
                            break search; // this isn't a warp move and piece can currently only warp
                        warp = false;
                    }
                    else {
                        if(piece.hasWarped)
                            break search; // can't warp twice :/
                        warp = true;
                    }

                    Location new_loc = new Location(x, y, z);
                    if(containsUnjumpable(new_loc) && containsImpassable(new_loc)) {
                        break search; // an unjumpable and impassable is found, piece can't move any further in that direction
                    }
                    else if(containsMovable(new_loc)) {
                        canMoveTo = false;
                        // check if piece is not already in jump or warp mode
                        // and if an enemy is there not in a safe zone
                        if(!jump && !warp && containsMovableEnemy(new_loc) && !containsSafe(new_loc)) {
                            jump = true;
                            range += piece.jumpRange; // increase range by jump range so piece can jump
                        }
                    }
                    
                    // if piece can move to this spot, add it to list
                    if(canMoveTo &&
                            // jumps will be added no matter what
                            (jump ||
                            // if there are jumps, moves that are not jumps won't be added
                            (!jumpsIncluded &&
                            // if only jumps and warps are allowed, this will allow warp moves
                            (warp || !jumpsAndWarpsOnly))
                            )) {
                        ArrayList<Location> locations = new ArrayList();
                        locations.add(new Location(piece.location.x, piece.location.y, piece.location.board));
                        if(jump) {
                            // if this is a jump, then at least one jump exists in the list of available moves
                            if(!jumpsIncluded) {
                                jumpsIncluded = true;
                                // if there are jumps, THERE CAN BE ONLY JUMPS
                                // travel backward through the list to erase moves that are not jumps
                                for(int k = moves.size()-1; k >= 0; k--)
                                    if(moves.get(k).locations.size() < 3)
                                        moves.remove(k);
                            }
                            
                            // if this is a jump, calculate the previous locations to get the jumped pieces
                            // currently, a piece can only jump once piece at a time, so only one jump location will be added here
                            for(int k = i-1; k > 0; k--) {
                                int x2 = piece.location.x+k*dir.x;
                                int y2 = piece.location.y+k*dir.y;
                                int z2 = piece.location.board+k*dir.board;
                                locations.add(new Location(x2, y2, z2));
                            }
                        }
                        locations.add(new_loc);
                        moves.add(new Move(locations, validateMine(new_loc), validateKing(new_loc, piece)));

                        // only one move per direction..
                        break search;
                    }
                
                // in case one of the new locations goes out of bounds
                } catch(IndexOutOfBoundsException e) {
                    break search;
                }
            }
        }

        // Pieces that can warp and move in the same turn have the option not to
        // no need to check if it can warp and move, if a piece can't,
        // then this method would have already returned an empty list if the move was over 1
        // jumps are forced, a piece has to jump, so to have this option
        // jumpsIncluded must be false
        if(moves.size() > 0 && move > 1 && !jumpsIncluded) {
            // so add a move with the Piece's current Location
            // as a starting position and destination
            Location loc = new Location(piece.location.x, piece.location.y, piece.location.board);
            ArrayList<Location> locations = new ArrayList();
            locations.add(loc);
            locations.add(loc);
            moves.add(new Move(locations, false, false));
        }
        
        return moves;
    }
    
    // searches the Boards for pieces that can be moved
    ArrayList<Piece> getMovablePieces() {
        ArrayList<Piece> pieces = new ArrayList();
        ArrayList<Boolean> jumps = new ArrayList();
        boolean canMoveTo, jump, jumpsIncluded = false;
        
        for(Board board: boards) {
            for(Piece piece: board.Pieces) {
                if(isPlayers(piece)) {
                    piece:
                    // goes through each direction a piece can travel to
                    for(Location dir: piece.movePositions) {
                        int range = piece.moveRange;
                        jump = false;
                        search:
                        for(int i = 1; i != range+1; i++) {
                            try {
                                canMoveTo = true; // assume piece can move here and then search otherwise
                                int x = piece.location.x+i*dir.x;
                                int y = piece.location.y+i*dir.y;
                                int z = piece.location.board+i*dir.board;
                                Location new_loc = new Location(x, y, z);

                                if(containsUnjumpable(new_loc) && containsImpassable(new_loc)) {
                                    break search; // an unjumpable and impassable is found, piece can't move any further in that direction
                                }
                                // check for movable, ally or foe the piece can't move here if one is found
                                else if(containsMovable(new_loc)) {
                                    canMoveTo = false;
                                    // checks if enemy is found in this spot, if it is, then this is a potential jump
                                    // only allows a piece to jump one enemy
                                    if(!jump && containsMovableEnemy(new_loc) && !containsSafe(new_loc)) {
                                        jump = true;
                                        range += piece.jumpRange; // increase range by jump range so piece can jump
                                    }
                                }

                                // if piece can move, add it to list of moveable pieces
                                if(canMoveTo && (jump || !jumpsIncluded)) {
                                    if(jump) {
                                        // if this is a jump, then at least one jump exists in the list of available moves
                                        if(!jumpsIncluded) {
                                            jumpsIncluded = true;
                                            // travel backward through the list to erase moves that are not jumps
                                            for(int k = jumps.size()-1; k >= 0; k--) {
                                                if(jumps.get(k) == false) {
                                                    pieces.remove(k);
                                                    jumps.remove(k);
                                                }
                                            }
                                        }

                                        jumps.add(true);
                                        pieces.add(piece);
                                        // break out of this piece since a move was found AND it's a jump
                                        break piece;
                                        
                                        // checks if previous piece added is the same piece
                                    } else if(pieces.size() == 0 || !pieces.get(pieces.size()-1).equals(piece)) {
                                        jumps.add(false);
                                        pieces.add(piece);
                                    }
                                }

                            // in case one of the new locations goes out of bounds
                            } catch(IndexOutOfBoundsException e) {
                                break search;
                            }
                        }
                    }
                }
            }
        }

        return pieces;
    }

    // determines how many more a certain piece type can be placed
    Integer getPiecesAvailable(Piece piece) {
        Counter b = getPieceCounter(piece.getClass().getName(), piece.owner);
        if(b !=  null)
            return b.n;
        else
            return 0;
    }

    Integer getPiecesAvailable(String piece, String owner) {
        return getPieceCounter(piece, owner).n;
    }

    // sets number of pieces to be placed by each player
    void setPieces(ArrayList<Player> players) {
        piecesAvailable = new ArrayList();

        // edit these to add custom piece amounts
        int     checker = boards.get(0).size.height <= 8 ? 9 : 14,
                king = 1,
                mine = 2,
                blocked = 2,
                safe = 1;

        for(Player player: players) {
            piecesAvailable.add(new Counter("Checker", player.getName(), checker));
            piecesAvailable.add(new Counter("King", player.getName(), king));
            piecesAvailable.add(new Counter("Mine", player.getName(), mine));
            piecesAvailable.add(new Counter("BlockedZone", player.getName(), blocked));
            piecesAvailable.add(new Counter("SafeZone", player.getName(), safe));
        }
    }
    
    // validates an addition of a piece to a location on a board
    // returns an error if piece cannot be added to this Location, null otherwise
    String validateAddition(Location loc, Piece piece) {
        // check if there are pieces available
        if(getPiecesAvailable(piece) == 0)
            return "There are no more " + piece.getClass().getName() + "s available.";
        // check bounds of location
        try {
            // checks if this is an impassable piece and this location already contains one
            if(!piece.passable && containsImpassable(loc))
                return "This location already contains a " + getImpassable(loc).getClass().getName() + ".";
            // checks if piece is not movable and a non-movable piece is already here
            if(piece.moveRange == 0 && containsImmovable(loc))
                return "This Location already contains a " + getImmovable(loc).getClass().getName() + ".";
        } catch(IndexOutOfBoundsException e) {
            return "Incorrect Location passed.";

        }
        // check if piece is placed on the correct side of board
        // for now direction=1 means Pieces moves down and belongs on top
        // were originally (height/2)-1 but had to subtract 1 from one and add 1 to another
        if(piece.direction == 1 && loc.y > (boards.get(loc.board).size.height/2)-2)
            return "Piece may only be placed on the top side of a Board.";
        if(piece.direction == -1 && loc.y <= (boards.get(loc.board).size.height/2))
            return "Piece may only be placed on the bottom side of a Board.";
        // checks if this spot is a black spot, top left is assumed to be black
        // if both x and y are not odd, or both even, then this is a white space
        if(loc.x%2 == loc.y%2)
            return "Pieces can only be placed on dark spaces.";
        else {
            getPieceCounter(piece.getClass().getName(), piece.owner).n--;
            return null;
        }
    }
    
    // checks if a location contains a mine and moveable ally and returns true if it does
    Boolean validateMine(Location loc) {
        if(containsEnemyMine(loc))
            return true;
        else
            return false;
    }
    
    Boolean validateKing(Location loc, Piece piece) {
        if(!piece.getClass().getName().equals("Checker"))
            return false;
        if(piece.direction == -1 && loc.y == 0)
            return true;
        if(piece.direction == 1 && loc.y == boards.get(loc.board).size.height-1)
            return true;
        else
            return false;
    }

    private Counter getPieceCounter(String piece, String player) {
        for(Counter c: piecesAvailable) {
            // looks for piece type and returns counter if player is the same or a dummy player
            if(c.name.equals(piece) && c.player.equals(player)) {
                return c;
            }
        }
        return null;
    }

    Boolean containsImpassable(Location loc) {
        if(getImpassable(loc) != null)
            return true;
        else
            return false;
    }

    Piece getImpassable(Location loc) {
        for(Piece piece: boards.get(loc.board).Positions[loc.x][loc.y])
            if(!piece.passable)
                return piece;

        return null;
    }
    
    Boolean containsSafe(Location loc) {
        if(getSafe(loc) != null)
            return true;
        else
            return false;
    }

    Piece getSafe(Location loc) {
        for(Piece piece: boards.get(loc.board).Positions[loc.x][loc.y])
            if(piece.safeOnContact)
                return piece;

        return null;
    }
    
    Boolean containsEnemyMine(Location loc) {
        if(getEnemyMine(loc) != null)
            return true;
        else
            return false;
    }
    
    Piece getEnemyMine(Location loc) {
        for(Piece piece: boards.get(loc.board).Positions[loc.x][loc.y])
            if(piece.explodeOnEnemyContact && piece.destroyEnemyOnContact && isEnemy(piece))
                return piece;
        
        return null;
    }
    
    Boolean containsUnjumpable(Location loc) {
        if(getUnjumpable(loc) != null)
            return true;
        else
            return false;
    }

    Piece getUnjumpable(Location loc) {
        for(Piece piece: boards.get(loc.board).Positions[loc.x][loc.y])
            if(!piece.jumpable)
                return piece;

        return null;
    }

    Boolean containsImmovable(Location loc) {
        if(getImmovable(loc) != null)
            return true;
        else
            return false;
    }
    
    Piece getImmovable(Location loc) {
        for(Piece piece: boards.get(loc.board).Positions[loc.x][loc.y])
            if(piece.moveRange == 0)
                return piece;

        return null;
    }

    Boolean containsMovable(Location loc) {
        if(getMovable(loc) != null)
            return true;
        else
            return false;
    }
    
    Piece getMovable(Location loc) {
        for(Piece piece: boards.get(loc.board).Positions[loc.x][loc.y])
            if(piece.moveRange != 0)
                return piece;
        
        return null;
    }
    
    Boolean containsMovableEnemy(Location loc) {
        if(getMovableEnemy(loc) != null)
            return true;
        else
            return false;
    }
    
    Boolean containsMovableAlly(Location loc) {
        if(getMovableAlly(loc) != null)
            return true;
        else
            return false;
    }

    Piece getMovableEnemy(Location loc) {
        for(Piece piece: boards.get(loc.board).Positions[loc.x][loc.y])
            if(piece.moveRange != 0 && isEnemy(piece))
                return piece;

        return null;
    }
    
    Piece getMovableAlly(Location loc) {
        for(Piece piece: boards.get(loc.board).Positions[loc.x][loc.y])
            if(piece.moveRange != 0 && isAlly(piece))
                return piece;
        
        return null;
    }

    Boolean isPlayers(Piece piece) {
        if(piece.owner.equals(currentPlayer.getName()))
            return true;
        else
            return false;
    }

    // made this in case someday there are teams of Players who are allies
    Boolean isAlly(Piece piece) {
        return isPlayers(piece);
    }

    Boolean isEnemy(Piece piece) {
        if(!piece.owner.equals(currentPlayer.getName()))
            return true;
        else
            return false;
    }
}